//  Gant -- A Groovy way of scripting Ant tasks.
//
//  Copyright © 2006–2011, 2013, 2018  Russel Winder
//
//  Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in
//  compliance with the License. You may obtain a copy of the License at
//
//    http://www.apache.org/licenses/LICENSE-2.0
//
//  Unless required by applicable law or agreed to in writing, software distributed under the License is
//  distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
//  implied. See the License for the specific language governing permissions and limitations under the
//  License.

package org.codehaus.gant;

import java.util.HashSet;
import java.util.List;
import java.util.Set;

//////////////////////////////////////////////////////////////////////////////////////////////////////////
//  In Groovy 1.7.x Closure was a type, in Groovy 1.8.x Closure is a parameterized type.
//  To support compilation against both versions of Groovy with the same source,
//  suffer the "raw type" warnings that Eclipse issues.
//////////////////////////////////////////////////////////////////////////////////////////////////////////

import groovy.lang.Closure;
import groovy.lang.DelegatingMetaClass;
import groovy.lang.GString;
import groovy.lang.MetaClass;
import groovy.lang.MissingMethodException;
import groovy.lang.MissingPropertyException;
import groovy.lang.Tuple;

import org.codehaus.groovy.runtime.MetaClassHelper;

import org.apache.tools.ant.BuildException;

/**
 *  This class is the metaclass used for target {@code Closure}s, and any enclosed {@code Closures}.
 *
 *  <p>This metaclass deals with {@code depends} method calls and redirects unknown method calls to the
 *  instance of {@code GantBuilder}.  To process the {@code depends} all closures from the
 *  binding called during execution of the Gant specification must be logged so that when a depends happens
 *  the full closure call history is available.</p>
 *
 *  @author Russel Winder
 */
public class GantMetaClass extends DelegatingMetaClass {
  /**
   *  The set of all targets that have been called.  This is a global variable shared by all instances of
   *  {@code GantMetaClass}.
   *
   *  <p>TODO : This code is a long way from thread safe, and so it needs fixing.  Should this variable be
   *  moved to the GantState class, which is the class that holds other bits of the internal shared state?
   *  Should a different data structure be used, one that is a bit more thread safe?  Arguably it is
   *  reasonable for this to be a synchronized object.</p>
   */
  private static final Set<Closure<?>> methodsInvoked = new HashSet<Closure<?>>();
  /**
   *  The binding (aka global shared state) that is being used.
   */
  private final GantBinding binding;
  /*
   */
  public GantMetaClass(final MetaClass metaClass, final GantBinding binding) {
    super(metaClass);
    this.binding = binding;
  }
  /**
   *  Execute a {@code Closure} only if it hasn't been executed previously.  If it is executed, record
   *  the execution.  Only used for processing a {@code depends} call.
   *
   *  @param closure The {@code Closure} to potentially call.
   *  @return the result of the {@code Closure} call, or {@code null} if the closure was not
   *  called.
   */
  private Object processClosure(final Closure<?> closure) {
    if (! methodsInvoked.contains(closure)) {
      methodsInvoked.add(closure);
      return closure.call();
    }
    return null;
  }
  /**
   *  Process the argument to a {@code depends} call.  If the parameter is a {@code Closure} just
   *  process it. If it is a {@code String} then do a lookup for the {@code Closure} in the
   *  binding, and if found process it.
   *
   *  @param argument The argument.
   *  @return The result of the {@code Closure}.
   */
  private Object processArgument(final Object argument) {
    final Object returnObject;
    if (argument instanceof Closure) { returnObject = processClosure((Closure<?>) argument); }
    else {
      final String errorReport = "depends called with an argument (" + argument + ") that is not a known target or list of targets.";
      Object theArgument = argument;
      if (theArgument instanceof GString) { theArgument = theArgument.toString(); }
      if (theArgument instanceof String) {
        final Object entry = binding.getVariable((String) theArgument);
        if ((entry != null) && (entry instanceof Closure)) { returnObject = processClosure((Closure<?>) entry); }
        else { throw new RuntimeException(errorReport); }
      }
      else { throw new RuntimeException(errorReport); }
    }
    return returnObject;
  }
  /**
   *  Invokes a method on the given object with the given name and arguments. The {@code MetaClass}
   *  will attempt to pick the best method for the given name and arguments. If a method cannot be invoked a
   *  {@code MissingMethodException} will be thrown.
   *
   *  @see MissingMethodException
   *  @param object The instance on which the method is invoked.
   *  @param methodName The name of the method.
   *  @param arguments The arguments to the method.
   *  @return The return value of the method which is {@code null} if the return type is
   *  {@code void}.
   */
  @Override public Object invokeMethod(final Object object, final String methodName, final Object[] arguments) {
    Object returnObject = null;
    if (methodName.equals("depends")) {
      for (final Object argument : arguments) {
        if (argument instanceof List<?>) {
          for (final Object item : (List<?>) argument) { returnObject = processArgument(item); }
        }
        else { returnObject = processArgument(argument); }
      }
    }
    else {
      try {
        returnObject = super.invokeMethod(object, methodName, arguments);
        try {
          final Closure<?> closure = (Closure<?>) binding.getVariable(methodName);
          if (closure != null) { methodsInvoked.add(closure); }
        }
        catch (final MissingPropertyException mpe) { /* Purposefully empty */ }
      }
      catch (final MissingMethodException mme) {
        try { returnObject = ((GantBuilder)(binding.getVariable("ant"))).invokeMethod(methodName, arguments); }
        catch (final BuildException be) {
          //  This BuildException could be a real exception due to a failed execution of a found Ant task
          //  (in which case it should be propagated), or it could be due to a failed name lookup (in which
          //  case the MissingMethodException should be propagated).  The big problem is distinguishing the
          //  various uses of Build Exception here -- for now use string search of the exception message to
          //  distinguish the cases.  NB GANT-49 and GANT-68 are the main conflicting issues here :-(
          if (be.getMessage().startsWith("Problem: failed to create task or type")) { throw mme; }
          else { throw be; }
        }
        catch (final Exception e) { throw mme; }
      }
    }
    return returnObject;
  }
  /**
   *  Invokes a method on the given object, with the given name and single argument.
   *
   *  @see #invokeMethod(Object, String, Object[])
   *  @param object The Object to invoke the method on
   *  @param methodName The name of the method
   *  @param arguments The argument to the method
   *  @return The return value of the method which is null if the return type is void
   */
  @Override public Object invokeMethod(final Object object, final String methodName, final Object arguments) {
    if (arguments == null) { return invokeMethod(object, methodName, MetaClassHelper.EMPTY_ARRAY); }
    else if (arguments instanceof Tuple) { return invokeMethod(object, methodName,((Tuple)arguments).toArray()); }
    else if (arguments instanceof Object[]) { return invokeMethod(object, methodName, (Object[])arguments); }
    else { return invokeMethod(object, methodName, new Object[] { arguments }); }
  }
  /**
   *  Invoke the given method.
   *
   *  @param name the name of the method to call
   *  @param args the arguments to use for the method call
   *  @return the result of invoking the method
   */
  @Override public Object invokeMethod(final String name, final Object args) {
    return invokeMethod(this, name, args);
  }
  //////////////////////////////////////////////////////////////////////////////////////////////////////////
  //  As at 2012-05-31 it is believed that groovy.lang.DelegatingMetaClass (the invokeMethod method
  //  anyway) has not been marked up for Java generics in any branch of Groovy (1.8, 2.0, master).
  //  This leads to the problem that blah(Class<?>) does not override blah(Class). So we must leave
  //  the Class type without a type parameter and suffer the compilation warning.
  //
  //  TODO : Class -> Class<?> when the Eclipse plugin and the Groovy code base allow.
  //////////////////////////////////////////////////////////////////////////////////////////////////////////
  /**
   *  Invoke a method on the given receiver for the specified arguments. The sender is the class that
   *  invoked the method on the object.  Attempt to establish the method to invoke based on the name and
   *  arguments provided.
   *
   *  <p>The {@code isCallToSuper} and {@code fromInsideClass} help the Groovy runtime perform
   *  optimizations on the call to go directly to the superclass if necessary.</p>
   *
   *  @param sender The {@code java.lang.Class} instance that invoked the method.
   *  @param receiver The object which the method was invoked on.
   *  @param methodName The name of the method.
   *  @param arguments The arguments to the method.
   *  @param isCallToSuper Whether the method is a call to a superclass method.
   *  @param fromInsideClass Whether the call was invoked from the inside or the outside of the class.
   *  @return The return value of the method
   */
  @SuppressWarnings("rawtypes")
  @Override public Object invokeMethod(final Class sender, final Object receiver, final String methodName, final Object[] arguments, final boolean isCallToSuper, final boolean fromInsideClass) {
    return invokeMethod(receiver, methodName, arguments);
  }
}
